package com.nf.mvc.argument;

import com.nf.mvc.HandlerContext;
import com.nf.mvc.MethodArgumentResolver;
import com.nf.mvc.support.HttpMethod;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Arrays;
import java.util.function.Supplier;

import static com.nf.mvc.HandlerContext.getHandlerContext;

/**
 * 此解析器主要是用来解析常见的Servlet相关的类型，比如HttpServletRequest，HttpSession等
 *
 * @see HandlerContext
 */
public class ServletApiMethodArgumentResolver implements MethodArgumentResolver {
    @Override
    public boolean supports(MethodParameter parameter) {
        Class<?> paramType = parameter.getType();
        return Arrays.stream(ServletApiEnum.values()).anyMatch(api -> api.getSupportedClass().isAssignableFrom(paramType));
    }

    /**
     * 参数解析，是从请求中获取数据的，所以方法设计没有response对象是合理的
     * 但这样给我们带来一个解析的问题，无法获取到response对象
     * request与response对象必须来自于DispatcherServlet的service
     *
     * @param parameter MethodParameter
     * @param request   请求对象
     * @return 解析之后的值
     * @throws Exception 解析过程中可能抛出的异常
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, HttpServletRequest request) throws Exception {
        Class<?> paramType = parameter.getType();
        // supports方法返回true才会调用resolveArgument方法，所以ServletApiEnum.of(paramType)不会返回null
        return ServletApiEnum.of(paramType).getValue();
    }

    /**
     * 枚举项的名字必须是支持类型的简单名字，这样才能使用ServletApiEnum.valueOf(paramType.getSimpleName())
     * 来获取枚举实例，否则你就只能用{@link #of(Class)}来获取枚举，这样就不要求枚举项是支持类型的简单名
     *
     * <p>枚举作为内部类加static关键字是多余的</p>
     * <p><b>注意：这个类设计出来主要是为了演示通过enum来优化if过多的技巧</b></p>
     *
     * <h3>枚举的知识点</h3>
     * <p><a href="https://www.baeldung.com/a-guide-to-java-enums">枚举要掌握的基本知识点</a>,
     * 枚举的每一个常量都是全局单例的，有多少个常量，对应的构造函数会执行多少次，比如下面的枚举4个常量，其构造函数会执行四次</p>
     * <h3>值为什么用Supplier函数接口而不直接用Object</h3>
     * <p>把字段valueSupplier的类型改为Object，每个常量的“()->” 这一部分代码去掉也是可以实现目标的，
     * 其它代码修改好之后，你测试会发现也没有什么问题，一切正常，但这个是有隐含bug的。</p>
     *
     * <p>要理解用Object作为字段类型为什么正常，首先你要知道api这些类型是单例的，比如HttpServletRequest，在容器启动后，
     * 你请求任何地址，测试其HttpServletRequest你都会发现其hashcode值是一样，但这个请求对象的内容是不一样的</p>
     *
     * <p>由于枚举是单例的，第一次请求导致枚举创建出来的话，value值就确定了，永不再改动，也就是getHandlerContext.getRequest
     * 之类的代码只会执行一次，那么后续请求得到的api类型对象应该是第一次的对象，应该会有bug呀！</p>
     *
     * <p>为什么没有出现明显的bug就是因为这些api类型本身就是单例的，不管哪次请求，API对象本来就是同一个，
     * 只不过这些对象在每次新请求发生时，其内容被servlet容器修改过了，你可以理解为把api对象内部存放数据的字节数组的内容改变了，api对象还是那一个</p>
     *
     * <p>如果这些api对象不是单例的，那么把Object类型作为valueSupplier字段的类型是有bug的，
     * 这里我采用Supplier<Object>就是为了解决这些隐藏bug考虑的，同时也演示一个重要的知识点：lambda有代码延迟执行的效果，
     * 每次在调用getValue方法时才导致lambda表达式才开始执行，也就是getHandlerContext().getXxx方法得到执行，
     * 这样才可以100%确保得到的对象就是放在HandlerContext对象里的API类型对象</p>
     */
    private enum ServletApiEnum {
        HttpServletRequest(HttpServletRequest.class, () -> getHandlerContext().getRequest()),
        HttpServletResponse(HttpServletResponse.class, () -> getHandlerContext().getResponse()),
        HttpSession(HttpSession.class, () -> getHandlerContext().getSession()),
        ServletContext(ServletContext.class, () -> getHandlerContext().getApplication());

        private final Class<?> supportedClass;
        private final Supplier<Object> valueSupplier;

        ServletApiEnum(Class<?> supportedClass, Supplier<Object> valueSupplier) {
            this.supportedClass = supportedClass;
            this.valueSupplier = valueSupplier;
        }

        public Class<?> getSupportedClass() {
            return supportedClass;
        }

        public Object getValue() {
            return this.valueSupplier.get();
        }

        /**
         * 此方法依据类型来返回对应的枚举实例的，枚举类自带的valueOf方法是一种严格相等的形式
         * 来返回对应的枚举实例，这里是看apiClass是不是可以赋值给对应枚举实例支持的类型
         * <p>此方法可以像{@link HttpMethod}那样继续进行改进，避免调用此方法时反复遍历此枚举的values,
         * {@link HttpMethod}的解决办法是在类的静态代码块中把枚举的各实例放置到Map集合中</p>
         *
         * @param apiClass api类型
         * @return 返回对应的枚举实例
         */
        public static ServletApiEnum of(Class<?> apiClass) {
            ServletApiEnum[] values = ServletApiEnum.values();
            for (ServletApiEnum value : values) {
                if (value.getSupportedClass().isAssignableFrom(apiClass)) {
                    return value;
                }
            }
            return null;
        }
    }
}
